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:
@@ -1,37 +1,73 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.pyo
|
||||
*.pyd
|
||||
*$py.class
|
||||
*.so
|
||||
*.egg-info/
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
.DS_Store?
|
||||
._*
|
||||
.Spotlight-V100
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Git
|
||||
.git/
|
||||
.gitignore
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
**/__pycache__/
|
||||
**/*.pyc
|
||||
**/*.pyo
|
||||
**/*.pyd
|
||||
# Logs
|
||||
logs/*.log
|
||||
|
||||
# Local settings
|
||||
settings_example.ini
|
||||
|
||||
# Databases and runtime files
|
||||
# Database
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
logs/
|
||||
|
||||
# Tests and artifacts
|
||||
.coverage
|
||||
# Tests
|
||||
tests/
|
||||
test_*.py
|
||||
.pytest_cache/
|
||||
htmlcov/
|
||||
**/tests/
|
||||
|
||||
# Stickers and large assets (if not needed at runtime)
|
||||
Stick/
|
||||
# Documentation
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Docker
|
||||
Dockerfile*
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
|
||||
42
.gitignore
vendored
42
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# Database files
|
||||
/database/tg-bot-database.db
|
||||
/database/tg-bot-database.db-shm
|
||||
/database/tg-bot-database.db-wal
|
||||
/database/tg-bot-database.db-wm
|
||||
/database/test.db
|
||||
/database/test.db-shm
|
||||
/database/test.db-wal
|
||||
@@ -10,7 +11,9 @@
|
||||
/settings.ini
|
||||
/myenv/
|
||||
/venv/
|
||||
/.idea/
|
||||
/.venv/
|
||||
|
||||
# Logs
|
||||
/logs/*.log
|
||||
|
||||
# Testing and coverage files
|
||||
@@ -32,6 +35,7 @@ test.db
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
@@ -44,9 +48,43 @@ test.db
|
||||
.Trashes
|
||||
ehthumbs.db
|
||||
Thumbs.db
|
||||
|
||||
# Documentation files
|
||||
PERFORMANCE_IMPROVEMENTS.md
|
||||
|
||||
# PID files
|
||||
*.pid
|
||||
helper_bot.pid
|
||||
voice_bot.pid
|
||||
|
||||
# Docker and build artifacts
|
||||
*.tar.gz
|
||||
prometheus-*/
|
||||
node_modules/
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.temp
|
||||
*.log
|
||||
*.pid
|
||||
|
||||
# Python cache
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
.tox/
|
||||
.cache/
|
||||
.mypy_cache/
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.9.6
|
||||
1
CHANGES_SUMMARY.md
Normal file
1
CHANGES_SUMMARY.md
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
37
Dockerfile
37
Dockerfile
@@ -1,37 +0,0 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
# Use a lightweight Python image
|
||||
FROM python:3.11-slim
|
||||
|
||||
# Prevent Python from writing .pyc files and enable unbuffered logs
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1
|
||||
|
||||
# Install system dependencies (if required by Python packages)
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends build-essential \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -m appuser \
|
||||
&& chown -R appuser:appuser /app
|
||||
|
||||
# Install Python dependencies first for better layer caching
|
||||
COPY requirements.txt ./
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Copy project files
|
||||
COPY . .
|
||||
|
||||
# Ensure runtime directories exist and are writable
|
||||
RUN mkdir -p logs database \
|
||||
&& chown -R appuser:appuser /app
|
||||
|
||||
# Switch to non-root user
|
||||
USER appuser
|
||||
|
||||
# Run the bot
|
||||
CMD ["python", "run_helper.py"]
|
||||
34
Dockerfile.bot
Normal file
34
Dockerfile.bot
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Установка системных зависимостей
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Создание рабочей директории
|
||||
WORKDIR /app
|
||||
|
||||
# Копирование requirements.txt
|
||||
COPY requirements.txt .
|
||||
|
||||
# Создание виртуального окружения
|
||||
RUN python -m venv .venv
|
||||
|
||||
# Обновление pip в виртуальном окружении
|
||||
RUN . .venv/bin/activate && pip install --upgrade pip
|
||||
|
||||
# Установка зависимостей в виртуальное окружение
|
||||
RUN . .venv/bin/activate && pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# Копирование исходного кода
|
||||
COPY . .
|
||||
|
||||
# Активация виртуального окружения
|
||||
ENV PATH="/app/.venv/bin:$PATH"
|
||||
ENV VIRTUAL_ENV="/app/.venv"
|
||||
|
||||
# Открытие порта для метрик
|
||||
EXPOSE 8000
|
||||
|
||||
# Команда запуска через виртуальное окружение
|
||||
CMD [".venv/bin/python", "run_helper.py"]
|
||||
124
Makefile
124
Makefile
@@ -1,88 +1,68 @@
|
||||
.PHONY: help test test-db test-coverage test-html clean install test-monitor
|
||||
.PHONY: help build up down logs clean restart status
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Available commands:"
|
||||
@echo " install - Install dependencies"
|
||||
@echo " test - Run all tests"
|
||||
@echo " test-db - Run database tests only"
|
||||
@echo " test-bot - Run bot startup and handler tests only"
|
||||
@echo " test-media - Run media handler tests only"
|
||||
@echo " test-errors - Run error handling tests only"
|
||||
@echo " test-utils - Run utility functions tests only"
|
||||
@echo " test-keyboards - Run keyboard and filter tests only"
|
||||
@echo " test-monitor - Test server monitoring module"
|
||||
@echo " test-coverage - Run tests with coverage report (helper_bot + database)"
|
||||
@echo " test-html - Run tests and generate HTML coverage report"
|
||||
@echo " clean - Clean up generated files"
|
||||
@echo " coverage - Show coverage report only"
|
||||
help: ## Показать справку
|
||||
@echo "🐍 Telegram Bot - Доступные команды (Python 3.9):"
|
||||
@echo ""
|
||||
@echo "🔧 Основные команды:"
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@echo ""
|
||||
@echo "📊 Мониторинг:"
|
||||
@echo " Prometheus: http://localhost:9090"
|
||||
@echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||
|
||||
# Install dependencies
|
||||
install:
|
||||
python3 -m pip install -r requirements.txt
|
||||
python3 -m pip install pytest-cov
|
||||
build: ## Собрать все контейнеры с Python 3.9
|
||||
docker-compose build
|
||||
|
||||
# Run all tests
|
||||
test:
|
||||
python3 -m pytest tests/ -v
|
||||
up: ## Запустить все сервисы с Python 3.9
|
||||
docker-compose up -d
|
||||
|
||||
# Run database tests only
|
||||
test-db:
|
||||
python3 -m pytest tests/test_db.py -v
|
||||
down: ## Остановить все сервисы
|
||||
docker-compose down
|
||||
|
||||
# Run bot tests only
|
||||
test-bot:
|
||||
python3 -m pytest tests/test_bot.py -v
|
||||
logs: ## Показать логи всех сервисов
|
||||
docker-compose logs -f
|
||||
|
||||
# Run media handler tests only
|
||||
test-media:
|
||||
python3 -m pytest tests/test_media_handlers.py -v
|
||||
logs-bot: ## Показать логи бота
|
||||
docker-compose logs -f telegram-bot
|
||||
|
||||
# Run error handling tests only
|
||||
test-errors:
|
||||
python3 -m pytest tests/test_error_handling.py -v
|
||||
logs-prometheus: ## Показать логи Prometheus
|
||||
docker-compose logs -f prometheus
|
||||
|
||||
# Run utils tests only
|
||||
test-utils:
|
||||
python3 -m pytest tests/test_utils.py -v
|
||||
logs-grafana: ## Показать логи Grafana
|
||||
docker-compose logs -f grafana
|
||||
|
||||
# Run keyboard and filter tests only
|
||||
test-keyboards:
|
||||
python3 -m pytest tests/test_keyboards_and_filters.py -v
|
||||
restart: ## Перезапустить все сервисы (с пересборкой Python 3.9)
|
||||
docker-compose down
|
||||
docker-compose build
|
||||
docker-compose up -d
|
||||
|
||||
# Test server monitoring module
|
||||
test-monitor:
|
||||
python3 tests/test_monitor.py
|
||||
status: ## Показать статус контейнеров
|
||||
docker-compose ps
|
||||
|
||||
# Test auto unban scheduler
|
||||
test-auto-unban:
|
||||
python3 -m pytest tests/test_auto_unban_scheduler.py -v
|
||||
check-python: ## Проверить версию Python в контейнере
|
||||
@echo "🐍 Проверяю версию Python в контейнере..."
|
||||
@docker exec telegram-bot .venv/bin/python --version || echo "Контейнер не запущен"
|
||||
|
||||
# Test auto unban integration
|
||||
test-auto-unban-integration:
|
||||
python3 -m pytest tests/test_auto_unban_integration.py -v
|
||||
test-compatibility: ## Тест совместимости с Python 3.8+
|
||||
@echo "🐍 Тестирую совместимость с Python 3.8+..."
|
||||
@python3 test_python38_compatibility.py
|
||||
|
||||
# Run tests with coverage
|
||||
test-coverage:
|
||||
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=term
|
||||
clean: ## Очистить все контейнеры и образы Python 3.9
|
||||
docker-compose down -v --rmi all
|
||||
docker system prune -f
|
||||
|
||||
# Run tests and generate HTML coverage report
|
||||
test-html:
|
||||
python3 -m pytest tests/ --cov=helper_bot --cov=database --cov-report=html:htmlcov --cov-report=term
|
||||
@echo "HTML coverage report generated in htmlcov/index.html"
|
||||
|
||||
# Show coverage report only
|
||||
coverage:
|
||||
python3 -m coverage report --include="helper_bot/*,database/*"
|
||||
|
||||
# Clean up generated files
|
||||
clean:
|
||||
rm -rf htmlcov/
|
||||
rm -f coverage.xml
|
||||
rm -f .coverage
|
||||
rm -f database/test.db
|
||||
rm -f test.db
|
||||
rm -f helper_bot.pid
|
||||
rm -f voice_bot.pid
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
start: build up ## Собрать и запустить все сервисы с Python 3.9
|
||||
@echo "🐍 Python 3.9 контейнер собран и запущен!"
|
||||
@echo "📊 Prometheus: http://localhost:9090"
|
||||
@echo "📈 Grafana: http://localhost:3000 (admin/admin)"
|
||||
@echo "🤖 Бот запущен в контейнере с Python 3.9"
|
||||
@echo "📝 Логи: make logs"
|
||||
|
||||
start-script: ## Запустить через скрипт start_docker.sh
|
||||
@echo "🐍 Запуск через скрипт start_docker.sh..."
|
||||
@./start_docker.sh
|
||||
|
||||
stop: down ## Остановить все сервисы
|
||||
@echo "🛑 Все сервисы остановлены"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
# This file makes the root directory a Python package
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ class AsyncBotDB:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def get_post_content(self, last_post_id: int) -> List[Tuple[str, str]]:
|
||||
async def get_post_content(self, last_post_id: int) -> List:
|
||||
"""Получение контента поста."""
|
||||
conn = None
|
||||
try:
|
||||
@@ -484,7 +484,7 @@ class AsyncBotDB:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def get_post_ids(self, last_post_id: int) -> List[int]:
|
||||
async def get_post_ids(self, last_post_id: int) -> List:
|
||||
"""Получение ID постов."""
|
||||
conn = None
|
||||
try:
|
||||
@@ -540,7 +540,7 @@ class AsyncBotDB:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def get_last_users(self, limit: int = 30) -> List[Tuple[str, int]]:
|
||||
async def get_last_users(self, limit: int = 30) -> List:
|
||||
"""Получение последних пользователей."""
|
||||
conn = None
|
||||
try:
|
||||
@@ -626,7 +626,7 @@ class AsyncBotDB:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def get_blacklist_users(self, offset: int = 0, limit: int = 10) -> List[Tuple[str, int, str, str]]:
|
||||
async def get_blacklist_users(self, offset: int = 0, limit: int = 10) -> List:
|
||||
"""Получение пользователей из черного списка."""
|
||||
conn = None
|
||||
try:
|
||||
@@ -658,7 +658,7 @@ class AsyncBotDB:
|
||||
if conn:
|
||||
await conn.close()
|
||||
|
||||
async def get_users_for_unban_today(self, date_to_unban: str) -> List[Tuple[int, str]]:
|
||||
async def get_users_for_unban_today(self, date_to_unban: str) -> List:
|
||||
"""Получение пользователей для разблокировки сегодня."""
|
||||
conn = None
|
||||
try:
|
||||
|
||||
@@ -6,6 +6,14 @@ from concurrent.futures import ThreadPoolExecutor
|
||||
|
||||
from logs.custom_logger import logger
|
||||
|
||||
# Local imports - metrics
|
||||
from helper_bot.utils.metrics import (
|
||||
metrics,
|
||||
track_time,
|
||||
track_errors,
|
||||
db_query_time
|
||||
)
|
||||
|
||||
|
||||
class BotDB:
|
||||
def __init__(self, current_dir, name):
|
||||
@@ -138,6 +146,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("add_new_user_in_db", "database")
|
||||
@track_errors("database", "add_new_user_in_db")
|
||||
@db_query_time("add_new_user_in_db", "our_users", "insert")
|
||||
def add_new_user_in_db(self, user_id: int, first_name: str, full_name: str, username: str, is_bot: bool,
|
||||
language_code: str, emoji: str, date_added: str, date_changed: str):
|
||||
"""
|
||||
@@ -189,6 +200,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("user_exists", "database")
|
||||
@track_errors("database", "user_exists")
|
||||
@db_query_time("user_exists", "our_users", "select")
|
||||
def user_exists(self, user_id: int):
|
||||
"""
|
||||
Проверяет, существует ли пользователь в базе данных.
|
||||
@@ -426,6 +440,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("get_info_about_stickers", "database")
|
||||
@track_errors("database", "get_info_about_stickers")
|
||||
@db_query_time("get_info_about_stickers", "our_users", "select")
|
||||
def get_info_about_stickers(self, user_id: int):
|
||||
"""
|
||||
Проверяет, получил ли пользователь стикеры.
|
||||
@@ -459,6 +476,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("update_info_about_stickers", "database")
|
||||
@track_errors("database", "update_info_about_stickers")
|
||||
@db_query_time("update_info_about_stickers", "our_users", "update")
|
||||
def update_info_about_stickers(self, user_id):
|
||||
"""
|
||||
Обновляет информацию о получении стикеров пользователем.
|
||||
@@ -623,6 +643,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("add_new_message_in_db", "database")
|
||||
@track_errors("database", "add_new_message_in_db")
|
||||
@db_query_time("add_new_message_in_db", "user_messages", "insert")
|
||||
def add_new_message_in_db(self, message_text: str, user_id: int, message_id: int, date: str):
|
||||
"""
|
||||
Добавляет новое сообщение пользователя в базу данных.
|
||||
@@ -657,6 +680,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("get_username_and_full_name", "database")
|
||||
@track_errors("database", "get_username_and_full_name")
|
||||
@db_query_time("get_username_and_full_name", "our_users", "select")
|
||||
def get_username_and_full_name(self, user_id: int):
|
||||
"""
|
||||
Получает full_name и username пользователя по ID из базы
|
||||
@@ -686,6 +712,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("update_username_and_full_name", "database")
|
||||
@track_errors("database", "update_username_and_full_name")
|
||||
@db_query_time("update_username_and_full_name", "our_users", "update")
|
||||
def update_username_and_full_name(self, user_id: int, username: str, full_name: str):
|
||||
"""
|
||||
Обновляет full_name и username пользователя
|
||||
@@ -715,6 +744,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("update_date_for_user", "database")
|
||||
@track_errors("database", "update_date_for_user")
|
||||
@db_query_time("update_date_for_user", "our_users", "update")
|
||||
def update_date_for_user(self, date: str, user_id: int):
|
||||
"""
|
||||
#TODO: Не возвращается ошибка sqlite3. Error. Тест не перехватывает. Возвращается no such table: our_users
|
||||
@@ -742,6 +774,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("check_emoji", "database")
|
||||
@track_errors("database", "check_emoji")
|
||||
@db_query_time("check_emoji", "our_users", "select")
|
||||
def check_emoji(self, emoji: str):
|
||||
"""
|
||||
Проверяет, есть ли уже такой emoji в таблице.
|
||||
@@ -767,6 +802,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("update_emoji_for_user", "database")
|
||||
@track_errors("database", "update_emoji_for_user")
|
||||
@db_query_time("update_emoji_for_user", "our_users", "update")
|
||||
def update_emoji_for_user(self, user_id: int, emoji: str):
|
||||
"""
|
||||
Обновляет эмодзи для пользователя в базе если его ранее не было установлено
|
||||
@@ -792,6 +830,9 @@ class BotDB:
|
||||
finally:
|
||||
self.close()
|
||||
|
||||
@track_time("check_emoji_for_user", "database")
|
||||
@track_errors("database", "check_emoji_for_user")
|
||||
@db_query_time("check_emoji_for_user", "our_users", "select")
|
||||
def check_emoji_for_user(self, user_id: int):
|
||||
"""
|
||||
Проверяет, есть ли уже у пользователя назначенный emoji.
|
||||
|
||||
66
docker-compose.yml
Normal file
66
docker-compose.yml
Normal file
@@ -0,0 +1,66 @@
|
||||
services:
|
||||
telegram-bot:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.bot
|
||||
container_name: telegram-bot
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8000:8000" # Экспозиция порта для метрик
|
||||
environment:
|
||||
- PYTHONPATH=/app
|
||||
volumes:
|
||||
- ./database:/app/database
|
||||
- ./logs:/app/logs
|
||||
- ./settings.ini:/app/settings.ini
|
||||
networks:
|
||||
- monitoring
|
||||
depends_on:
|
||||
- prometheus
|
||||
- grafana
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
ports:
|
||||
- "9090:9090"
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--storage.tsdb.retention.time=200h'
|
||||
- '--web.enable-lifecycle'
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- GF_SECURITY_ADMIN_USER=admin
|
||||
- GF_SECURITY_ADMIN_PASSWORD=admin
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
volumes:
|
||||
- grafana_data:/var/lib/grafana
|
||||
- ./grafana/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./grafana/datasources:/etc/grafana/provisioning/datasources
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- monitoring
|
||||
depends_on:
|
||||
- prometheus
|
||||
|
||||
volumes:
|
||||
prometheus_data:
|
||||
grafana_data:
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
driver: bridge
|
||||
12
grafana/dashboards/dashboards.yml
Normal file
12
grafana/dashboards/dashboards.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
- name: 'Telegram Bot Dashboards'
|
||||
orgId: 1
|
||||
folder: ''
|
||||
type: file
|
||||
disableDeletion: false
|
||||
updateIntervalSeconds: 10
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
577
grafana/dashboards/telegram-bot-dashboard.json
Normal file
577
grafana/dashboards/telegram-bot-dashboard.json
Normal file
@@ -0,0 +1,577 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"id": null,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"vis": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 1,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "rate(bot_commands_total[5m])",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Commands per Second",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"vis": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, rate(method_duration_seconds_bucket[5m]))",
|
||||
"refId": "A"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "histogram_quantile(0.99, rate(method_duration_seconds_bucket[5m]))",
|
||||
"refId": "B"
|
||||
}
|
||||
],
|
||||
"title": "Method Response Time (P95, P99)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"vis": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 8
|
||||
},
|
||||
"id": 3,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "rate(errors_total[5m])",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Errors per Second",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"vis": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 8
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "active_users",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Active Users",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"vis": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "s"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 5,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "histogram_quantile(0.95, rate(db_query_duration_seconds_bucket[5m]))",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Database Query Time (P95)",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 10,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"vis": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "never",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
},
|
||||
"unit": "short"
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 8,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 16
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom"
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "prometheus",
|
||||
"uid": "PBFA97CFB590B2093"
|
||||
},
|
||||
"expr": "rate(messages_processed_total[5m])",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Messages Processed per Second",
|
||||
"type": "timeseries"
|
||||
}
|
||||
],
|
||||
"refresh": "5s",
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [
|
||||
"telegram",
|
||||
"bot",
|
||||
"monitoring"
|
||||
],
|
||||
"templating": {
|
||||
"list": []
|
||||
},
|
||||
"time": {
|
||||
"from": "now-1h",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Telegram Bot Dashboard",
|
||||
"uid": "telegram-bot",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
8
grafana/datasources/prometheus.yml
Normal file
8
grafana/datasources/prometheus.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
url: http://prometheus:9090
|
||||
isDefault: true
|
||||
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
|
||||
26
prometheus.yml
Normal file
26
prometheus.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
rule_files:
|
||||
# - "first_rules.yml"
|
||||
# - "second_rules.yml"
|
||||
|
||||
scrape_configs:
|
||||
- job_name: 'telegram-bot'
|
||||
static_configs:
|
||||
- targets: ['telegram-bot:8000']
|
||||
metrics_path: '/metrics'
|
||||
scrape_interval: 10s
|
||||
scrape_timeout: 10s
|
||||
honor_labels: true
|
||||
|
||||
- job_name: 'prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:9090']
|
||||
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets:
|
||||
# - alertmanager:9093
|
||||
@@ -1,3 +1,9 @@
|
||||
[project]
|
||||
name = "telegram-helper-bot"
|
||||
version = "1.0.0"
|
||||
description = "Telegram bot with monitoring and metrics"
|
||||
requires-python = ">=3.9"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = ["test_*.py"]
|
||||
|
||||
13
requirements-dev.txt
Normal file
13
requirements-dev.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
# Development and testing dependencies
|
||||
-r requirements.txt
|
||||
|
||||
# Testing
|
||||
pytest>=8.0.0
|
||||
pytest-asyncio>=0.23.0
|
||||
pytest-cov>=4.0.0
|
||||
coverage>=7.0.0
|
||||
|
||||
# Development tools
|
||||
black>=23.0.0
|
||||
flake8>=6.0.0
|
||||
mypy>=1.0.0
|
||||
@@ -13,10 +13,9 @@ psutil~=6.1.0
|
||||
# Scheduling
|
||||
apscheduler~=3.10.4
|
||||
|
||||
# Testing
|
||||
pytest==8.2.2
|
||||
pytest-asyncio==1.1.0
|
||||
coverage==7.5.4
|
||||
# Metrics and monitoring
|
||||
prometheus-client==0.19.0
|
||||
aiohttp==3.9.1
|
||||
|
||||
# Development tools
|
||||
pluggy==1.5.0
|
||||
|
||||
@@ -12,6 +12,7 @@ from helper_bot.main import start_bot
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from helper_bot.server_monitor import ServerMonitor
|
||||
from helper_bot.utils.auto_unban_scheduler import get_auto_unban_scheduler
|
||||
from helper_bot.utils.metrics_exporter import MetricsManager
|
||||
|
||||
|
||||
async def start_monitoring(bdf, bot):
|
||||
@@ -46,6 +47,9 @@ async def main():
|
||||
auto_unban_scheduler.set_bot(monitor_bot)
|
||||
auto_unban_scheduler.start_scheduler()
|
||||
|
||||
# Инициализируем метрики
|
||||
metrics_manager = MetricsManager(host="0.0.0.0", port=8000)
|
||||
|
||||
# Флаг для корректного завершения
|
||||
shutdown_event = asyncio.Event()
|
||||
|
||||
@@ -58,9 +62,10 @@ async def main():
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
signal.signal(signal.SIGTERM, signal_handler)
|
||||
|
||||
# Запускаем бота и мониторинг
|
||||
# Запускаем бота, мониторинг и метрики
|
||||
bot_task = asyncio.create_task(start_bot(bdf))
|
||||
monitor_task = asyncio.create_task(monitor.monitor_loop())
|
||||
metrics_task = asyncio.create_task(metrics_manager.start())
|
||||
|
||||
try:
|
||||
# Ждем сигнала завершения
|
||||
@@ -80,14 +85,18 @@ async def main():
|
||||
print("Останавливаем планировщик автоматического разбана...")
|
||||
auto_unban_scheduler.stop_scheduler()
|
||||
|
||||
print("Останавливаем метрики...")
|
||||
await metrics_manager.stop()
|
||||
|
||||
print("Останавливаем задачи...")
|
||||
# Отменяем задачи
|
||||
bot_task.cancel()
|
||||
monitor_task.cancel()
|
||||
metrics_task.cancel()
|
||||
|
||||
# Ждем завершения задач
|
||||
try:
|
||||
await asyncio.gather(bot_task, monitor_task, return_exceptions=True)
|
||||
await asyncio.gather(bot_task, monitor_task, metrics_task, return_exceptions=True)
|
||||
except Exception as e:
|
||||
print(f"Ошибка при остановке задач: {e}")
|
||||
|
||||
@@ -97,4 +106,12 @@ async def main():
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except AttributeError:
|
||||
# Fallback for Python 3.6-3.7
|
||||
loop = asyncio.get_event_loop()
|
||||
try:
|
||||
loop.run_until_complete(main())
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
92
run_metrics_only.py
Normal file
92
run_metrics_only.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Standalone metrics server for testing.
|
||||
Run this to start just the metrics system without the bot.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import signal
|
||||
import sys
|
||||
from helper_bot.utils.metrics_exporter import MetricsManager
|
||||
|
||||
|
||||
class MetricsServer:
|
||||
"""Standalone metrics server."""
|
||||
|
||||
def __init__(self, host: str = "0.0.0.0", port: int = 8000):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.metrics_manager = MetricsManager(host, port)
|
||||
self.running = False
|
||||
|
||||
async def start(self):
|
||||
"""Start the metrics server."""
|
||||
try:
|
||||
await self.metrics_manager.start()
|
||||
self.running = True
|
||||
print(f"🚀 Metrics server started on {self.host}:{self.port}")
|
||||
print(f"📊 Metrics endpoint: http://{self.host}:{self.port}/metrics")
|
||||
print(f"🏥 Health check: http://{self.host}:{self.port}/health")
|
||||
print(f"ℹ️ Info: http://{self.host}:{self.port}/")
|
||||
print("\nPress Ctrl+C to stop the server")
|
||||
|
||||
# Keep the server running
|
||||
while self.running:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error starting metrics server: {e}")
|
||||
raise
|
||||
|
||||
async def stop(self):
|
||||
"""Stop the metrics server."""
|
||||
if self.running:
|
||||
self.running = False
|
||||
await self.metrics_manager.stop()
|
||||
print("\n🛑 Metrics server stopped")
|
||||
|
||||
def signal_handler(self, signum, frame):
|
||||
"""Handle shutdown signals."""
|
||||
print(f"\n📡 Received signal {signum}, shutting down...")
|
||||
asyncio.create_task(self.stop())
|
||||
|
||||
|
||||
async def main():
|
||||
"""Main function."""
|
||||
# Parse command line arguments
|
||||
host = "0.0.0.0"
|
||||
port = 8000
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
host = sys.argv[1]
|
||||
if len(sys.argv) > 2:
|
||||
port = int(sys.argv[2])
|
||||
|
||||
# Create and start server
|
||||
server = MetricsServer(host, port)
|
||||
|
||||
# Setup signal handlers
|
||||
signal.signal(signal.SIGINT, server.signal_handler)
|
||||
signal.signal(signal.SIGTERM, server.signal_handler)
|
||||
|
||||
try:
|
||||
await server.start()
|
||||
except KeyboardInterrupt:
|
||||
print("\n📡 Keyboard interrupt received")
|
||||
finally:
|
||||
await server.stop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("🔧 Starting standalone metrics server...")
|
||||
print("Usage: python run_metrics_only.py [host] [port]")
|
||||
print("Default: host=0.0.0.0, port=8000")
|
||||
print()
|
||||
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\n🛑 Server stopped by user")
|
||||
except Exception as e:
|
||||
print(f"❌ Server error: {e}")
|
||||
sys.exit(1)
|
||||
32
start_docker.sh
Executable file
32
start_docker.sh
Executable file
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
|
||||
echo "🐍 Запуск Telegram Bot с Python 3.9 (стандартная версия)..."
|
||||
echo ""
|
||||
|
||||
echo "🔧 Сборка Docker образа с Python 3.9..."
|
||||
make build
|
||||
|
||||
echo ""
|
||||
echo "🚀 Запуск сервисов..."
|
||||
make up
|
||||
|
||||
echo ""
|
||||
echo "🐍 Проверка версии Python в контейнере..."
|
||||
make check-python
|
||||
|
||||
echo ""
|
||||
echo "📦 Проверка установленных пакетов..."
|
||||
docker exec telegram-bot .venv/bin/pip list
|
||||
|
||||
echo ""
|
||||
echo "✅ Сервисы успешно запущены!"
|
||||
echo ""
|
||||
echo "📝 Полезные команды:"
|
||||
echo " Логи бота: make logs-bot"
|
||||
echo " Статус: make status"
|
||||
echo " Остановка: make stop"
|
||||
echo " Перезапуск: make restart"
|
||||
echo ""
|
||||
echo "📊 Мониторинг:"
|
||||
echo " Prometheus: http://localhost:9090"
|
||||
echo " Grafana: http://localhost:3000 (admin/admin)"
|
||||
@@ -1,9 +0,0 @@
|
||||
import asyncio
|
||||
|
||||
from helper_bot.utils.base_dependency_factory import get_global_instance
|
||||
from voice_bot.main import start_bot
|
||||
|
||||
bdf = get_global_instance()
|
||||
|
||||
if __name__ == '__main__':
|
||||
asyncio.run(start_bot(get_global_instance()))
|
||||
Reference in New Issue
Block a user